// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Diagnostics.CodeAnalysis;
using System.Net;
using Azure.DataApiBuilder.Config.DatabasePrimitives;
using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Core.Parsers;
using Azure.DataApiBuilder.Core.Resolvers;
using Azure.DataApiBuilder.Service.Exceptions;
using HotChocolate.Language;

namespace Azure.DataApiBuilder.Core.Services
{
    /// <summary>
    /// Interface to retrieve information for the runtime from the database.
    /// </summary>
    public interface ISqlMetadataProvider
    {
        /// <summary>
        /// Initializes this metadata provider for the runtime.
        /// </summary>
        Task InitializeAsync();

        /// <summary>
        /// Obtains the underlying source object's schema name (SQL) or container name (Cosmos).
        /// </summary>
        string GetSchemaName(string entityName);

        bool VerifyForeignKeyExistsInDB(
            DatabaseTable databaseObjectA,
            DatabaseTable databaseObjectB);

        /// <summary>
        /// Obtains the underlying source object's name (SQL table or Cosmos container).
        /// </summary>
        string GetDatabaseObjectName(string entityName);

        (string, string) ParseSchemaAndDbTableName(string source);

        /// <summary>
        /// Obtains all the underlying column names for each entity.
        /// </summary>
        List<string> GetSchemaGraphQLFieldNamesForEntityName(string entityName);

        /// <summary>
        /// Obtains the underlying GraphQL object type for an entity field.
        /// </summary>
        string? GetSchemaGraphQLFieldTypeFromFieldName(string entityName, string fieldName);

        /// <summary>
        /// Obtains the underlying GraphQL object for an entity field.
        /// </summary>
        FieldDefinitionNode? GetSchemaGraphQLFieldFromFieldName(string entityName, string fieldName);

        /// <summary>
        /// Gets a collection of linking entities generated by DAB (required to support multiple mutations).
        /// </summary>
        IReadOnlyDictionary<string, Entity> GetLinkingEntities() => new Dictionary<string, Entity>();

        /// <summary>
        /// Obtains the underlying SourceDefinition for the given entity name.
        /// </summary>
        SourceDefinition GetSourceDefinition(string entityName);

        /// <summary>
        /// Obtains the underlying StoredProcedureDefinition for the given entity name.
        /// </summary>
        StoredProcedureDefinition GetStoredProcedureDefinition(string entityName);

        // Contains all the referencing and referenced columns for each pair
        // of referencing and referenced tables.
        public Dictionary<RelationShipPair, ForeignKeyDefinition>? PairToFkDefinition { get; }

        /// <summary>
        /// Maps {entityName, relationshipName} to the ForeignKeyDefinition defined for (currently) self-join relationships.
        /// The ForeignKeyDefinition denotes referencing/referenced fields and whether the referencing/referenced fields
        /// apply to the target or source entity as defined in the relationship config.
        /// </summary>
        public Dictionary<EntityRelationshipKey, ForeignKeyDefinition> RelationshipToFkDefinition { get; set; }

        public List<Exception> SqlMetadataExceptions { get; }

        Dictionary<string, DatabaseObject> EntityToDatabaseObject { get; set; }

        /// <summary>
        /// Contains mapping of exposed graphQL names of StoredProcedure to EntityName defined in the config.
        /// </summary>
        Dictionary<string, string> GraphQLStoredProcedureExposedNameToEntityNameMap { get; set; }

        /// <summary>
        /// Obtains the underlying OData parser.
        /// </summary>
        /// <returns></returns>
        ODataParser GetODataParser();

        /// <summary>
        /// For the entity that is provided as an argument,
        /// try to get the exposed name associated
        /// with the provided field, if it exists, save in out
        /// parameter, and return true, otherwise return false.
        /// If an entity name is provided that does not exist
        /// as metadata in this metadata provider, a KeyNotFoundException
        /// is thrown.
        /// </summary>
        /// <param name="entityName">The entity whose mapping we lookup.</param>
        /// <param name="backingFieldName">The field used for the lookup in the mapping.</param>
        /// <param name="name">Out parameter in which we will save exposed name.</param>
        /// <returns>True if exists, false otherwise.</returns>
        /// <throws>KeyNotFoundException if entity name not found.</throws>
        bool TryGetExposedColumnName(string entityName, string backingFieldName, [NotNullWhen(true)] out string? name);

        /// <summary>
        /// For the entity that is provided as an argument,
        /// try to get the underlying backing column name associated
        /// with the provided field, if it exists, save in out
        /// parameter, and return true, otherwise return false.
        /// If an entity name is provided that does not exist
        /// as metadata in this metadata provider, a KeyNotFoundException
        /// is thrown.
        /// </summary>
        /// <param name="entityName">The entity whose mapping we lookup.</param>
        /// <param name="field">The field used for the lookup in the mapping.</param>
        /// <param name="name"/>Out parameter in which we will save backing column name.</param>
        /// <returns>True if exists, false otherwise.</returns>
        /// <throws>KeyNotFoundException if entity name not found.</throws>
        bool TryGetBackingColumn(string entityName, string field, [NotNullWhen(true)] out string? name);

        /// <summary>
        /// Obtains the underlying database type.
        /// </summary>
        /// <returns></returns>
        DatabaseType GetDatabaseType();

        /// <summary>
        /// Method to access the dictionary since DatabaseObject is abstract class
        /// </summary>
        /// <param name="key">Key.</param>
        /// <returns>DatabaseObject.</returns>
        public virtual DatabaseObject GetDatabaseObjectByKey(string key)
        {
            return EntityToDatabaseObject[key];
        }

        IQueryBuilder GetQueryBuilder();

        /// <summary>
        /// Returns a dictionary of (EntityName, DatabaseObject).
        /// </summary>
        /// <returns></returns>
        public IReadOnlyDictionary<string, DatabaseObject> GetEntityNamesAndDbObjects();

        /// <summary>
        /// Given entity name, gets the map of exposed field to backing field mappings.
        /// </summary>
        public bool TryGetExposedFieldToBackingFieldMap(string entityName, [NotNullWhen(true)] out IReadOnlyDictionary<string, string>? mappings);

        /// <summary>
        /// Given entity name, gets the map of backing field to exposed field mappings.
        /// </summary>
        public bool TryGetBackingFieldToExposedFieldMap(string entityName, [NotNullWhen(true)] out IReadOnlyDictionary<string, string>? mappings);

        /// <summary>
        /// Gets Partition Key Path of a database container.
        /// </summary>
        string? GetPartitionKeyPath(string database, string container);

        /// <summary>
        /// Sets Partition Key Path of a database container.
        /// Example of a Partition Key Path looks like: /id
        /// Example of a Parition Key Path on nested inner object: /character/id
        /// When a partition key path is being looked up for the first time, this method will add it to the dictionary
        /// </summary>
        void SetPartitionKeyPath(string database, string container, string partitionKeyPath);

        /// <summary>
        /// The GraphQL type is expected to either match the top level entity name from runtime config or the name specified in the singular property 
        /// The entities dictionary should always be using the top level entity name
        /// First try to check if the GraphQL type is matching any key in the entities dictionary
        /// If no match found, then use the GraphQL singular type in the runtime config to look up the top-level entity name from a GraphQLSingularTypeToEntityNameMap
        /// </summary>
        public string GetEntityName(string graphQLType);

        /// <summary>
        /// For the given graphql type, returns the inferred database object.
        /// Does this by first looking up the entity name from the graphql type to entity name mapping.
        /// Subsequently, looks up the database object inferred for the corresponding entity.
        /// </summary>
        /// <param name="graphqlType">Name of the graphql type</param>
        /// <returns>Underlying inferred DatabaseObject.</returns>
        /// <exception cref="DataApiBuilderException">Thrown if entity is not found.</exception>
        public DatabaseObject GetDatabaseObjectForGraphQLType(string graphqlType)
        {
            string entityName = GetEntityName(graphqlType);

            if (!EntityToDatabaseObject.TryGetValue(entityName, out DatabaseObject? databaseObject))
            {
                throw new DataApiBuilderException(message: $"Source Definition for {entityName} has not been inferred.",
                    statusCode: HttpStatusCode.InternalServerError,
                    subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound);
            }

            return databaseObject;
        }

        /// <summary>
        /// Retrieves the default schema name for this metadata provider.
        /// </summary>
        public string GetDefaultSchemaName();

        /// <summary>
        /// Returns true when the engine is running in Development mode. When running in Production
        /// mode, it returns false.
        /// </summary>
        public bool IsDevelopmentMode();

        /// <summary>
        /// Initializes this metadata provider for the runtime.
        /// This method will take in various objects which we can use directly rather than recreating the objects
        /// using multiple tsql queries
        /// </summary>
        /// <param name="entityToDatabaseObject">maps entity to DatabaseObject</param>
        /// <param name="graphQLStoredProcedureExposedNameToEntityNameMap">Dictionary containing mapping of graphQL stored procedure exposed query/mutation name to their corresponding entity names defined in the config.</param>
        void InitializeAsync(
            Dictionary<string, DatabaseObject> entityToDatabaseObject,
            Dictionary<string, string> graphQLStoredProcedureExposedNameToEntityNameMap);

        /// <summary>
        /// Helper method to get the Foreign Key definition in the object definition of the source entity which relates it
        /// with the target entity. In the Foreign key definition, the table backing the referencing entity acts as the referencing table
        /// and the table backing the referenced entity acts as the referenced table.
        /// </summary>
        /// <param name="sourceEntityName">Source entity name.</param>
        /// <param name="targetEntityName">Target entity name.</param>
        /// <param name="referencedEntityName">Referenced entity name.</param>
        /// <param name="referencingEntityName">Referencing entity name.</param>
        /// <param name="foreignKeyDefinition">Stores the required foreign key definition from the referencing to referenced entity.</param>
        /// <param name="isMToNRelationship">Indicates whether the relationship type is M:N</param>
        /// <returns>true when the foreign key definition is successfully determined.</returns>
        /// <example>
        /// For a 1:N relationship between Publisher: Book entity defined in Publisher entity's config:
        /// sourceEntityName: Publisher (The entity in whose config the relationship is defined)
        /// targetEntityName: Book (The target.entity in the relationship config)
        /// referencingEntityName: Book (Entity holding foreign key reference)
        /// referencedEntityName: Publisher (Entity being referenced by foreign key).
        /// </example>
        public bool TryGetFKDefinition(
            string sourceEntityName,
            string targetEntityName,
            string referencingEntityName,
            string referencedEntityName,
            [NotNullWhen(true)] out ForeignKeyDefinition? foreignKeyDefinition,
            bool isMToNRelationship) => throw new NotImplementedException();
    }
}
